RoR_chapter5 テストをはじめよう
from 現場で使えるRuby on Rails 5 速習実践ガイド
RoR_chapter5 テストをはじめよう
初心者が思いつくテストの例
メソッドを作ったときは、コンソールで動作確認
webサーバを起動してブラウザで動作確認
自動テストの実装
目視確認は大事だが手間なので、細かい部分のテストを自動化する
コードが変更されるたびに自動テストを動かしエラーの有無を確認する
Railsの文脈ではテストは自動テストを指すことが多い。
テストのメリットなど
読んだ本のテスト駆動開発を見直すと、大体同じことが書いてある。
テストは手間だが、書いておけばコストの削減と今後変更する際にフットワークが軽くなる。
特にRailsにおいては、gemの存在がある
RubyやRails, gemのバージョンを最新版に上げることは開発上非常に大事
バージョンアップの際に自動テストがあるかないかで手間が大きく変わる。
RSpec
https://rspec.info/
テストをspec(仕様書)のような感覚で記述するという意味のライブラリ
specという用語がテストという意味も兼ねるので、使う際は注目しておく。
Capybara
webアプリのE2E(End-to-End)フレームワーク
Rails5.1から同梱されるようになった。
RSpecと組み合わせて使う
ブラウザ動作のシミュレーション、jsの動作まで含めたテストを行うことができる
DSLを使うことで手作業で確認していたような操作を記述して再現も可能
Domain Specific Language
https://anken-hyouban.com/blog/2021/09/13/dsl/
ドメイン固有言語(DSL)とは、特定の作業や問題解決を目的に設計されたプログラミング言語です。別名「ドメイン特化言語」とも呼ばれ、JavaやC言語などの汎用言語とは違い、特定の領域(ドメイン)に関連する処理や定義の記述に特化した仕様を持つプログラミング言語のことを表します。
特定の領域(ドメイン)に関連する処理や定義の記述に特化した仕様を持ちます。
SQL(データベース特化)とかVBA(MS製品特化)とかそういった言語のことを指す。
FactoryBot
テスト用データの作成をサポートするgem
同じくDSLで記述し、データを効率よく定義することができる。
テスト用語
システムテスト
E2Eテストとほぼ同義
https://circleci.com/ja/blog/what-is-end-to-end-testing/
E2E (エンドツーエンド) テストは、アプリケーションが期待どおりに動作し、ユーザーのタスクやプロセスの種類を問わずデータフローが適切に機能することを確認する手法です。
ブラウザを通してアプリ挙動を確かめる。
結合テスト
実装した機能が連続で動くかを確認する
システムテストよりもどちらかといえば内部的な確認
機能テスト
コントローラ単位のテスト
中でも大事なのはシステムテスト。そのほかは知りたくなったときにもう一度書籍を見る。(chapter5-4...)
システムテスト(System Spec)を書く準備
RSpecのインストール
gem 'rspec-rails', '~> 3.7'
bundle
code:console
bin/rails g rspec:install
Running via Spring preloader in process 66927
create .rspec
create spec
create spec/spec_helper.rb #全体的な設定を書くためのファイル
create spec/rails_helper.rb #rails特有の設定を書くファイル
rails環境を作った際に自動生成されていたtestディレクトリの削除
RSpecと似た機能を持つMinitestを使う場合は必要になるが、今回は使わないので消しておく。
rm -r ./test
Capybaraの初期準備
今回はrails newを環境構築で実行した際に入っている
RSpecで利用するための準備をする。
code:spec/spec_helper.rb
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
# 追加
require 'capybara/rspec'
RSpec.configure do |config|
# 追加
config:before(:each, type: :system) do
driven_by :selenium_chrome_headless
end
# rspec-expectations config goes here. You can use an alternate
RSpecでCapybaraを使うために必要な機能を読み込むための追加
System Specを実行するドライバの設定を行なった。
ドライバ: ここではブラウザ相当の機能を利用するためのプログラムを指す
FactoryBotのインストール
gemを入れれば良い。
gem 'factory_bot_rails', '~> 4.11'
bundleでOK
実際に書く
RSpecのフォーマットの基本
code:rspec
describe テスト対象, type: Specの種類 do
context 状況・状態 dp
before do
事前の準備
end
it 使用の内容 do
期待する動作
end
end
end
その他の文法もあるので、各用語について軽く触れる
describe
何についての使用を記述しようとしているのか
System Specであれば、一連動作によって達成したい機能や処理の名称などを書く
Model Specであれば、モデルクラス名やメソッド名などを記述する
context
テスト内容を状態ごとに分類するために利用する
System Specであれば
ユーザーがログインしているかしていないか
ユーザーの入力内容が正しいか正しくないかなど
it
期待する動作を、文章とコードで記述する
この中に書かれた動作通りに対象が動いたならば、Specが成功したといえる。
予期せぬ例外が出ればerrorとなり、エラーではないが想定していない挙動であればFailureとしてカウント
実行イメージ
https://scrapbox.io/files/6509aa474e7ce8001c044c06.jpg
大枠のdescribeがあり
その中にいくつか検証したい要素があり
要素ごとにcontextを書いて進める
code:RSpec
describe 'xx機能', type: :system do
describe '登録' do
context '〇〇の場合' do
before do
# contextを確認するために必要な事前準備
end
it 'xxする'
# 期待する動作
end
end
context ... do
...
end
end
describe '解約' do
...
end
end
FactoryBotでテストデータを作成する準備をする
検証のためにテストのデータをDBに用意することはよくある
Railsはテスト目的で使うデータベーるを切り離して管理している
development, test, productionのうちのtestを使う。
テストデータ準備のステップ
FactoryBotでデータ作成のためのテンプレートを作成
System Specの適切なbeforeで、FactoryBotを利用してテストDBにデータを挿入する
テンプレートファイルを作ろう
まずユーザーから(TaskはユーザーIDが必須なので、まずユーザーから作る)
自分で下記のファイルを作る。
code:3_chapter/taskleaf/spec/factories/users.rb
FactoryBot.define do
factory :user do
name { 'テストユーザー' }
email { 'test1@example.com' }
password { 'password' }
end
end
:user
factiryメソッドを使って、 Userクラスのファクトリを定義
クラスを:userという名前から自動で類推してくれる
自分で書きたいときはfactory :admin_user, class: User doというように書く。
タスクの方も作る
code:3_chapter/taskleaf/spec/factories/tasks.rb
FactoryBot.define do
factory :task do
name { 'テストを書く' }
description { 'RSpec & Capybara & FactoryBotを準備する' }
user
end
end
user
別ファイルで定義したuserのファクトリを、taskモデルに定義されたuserという名前の関連を生成するのに利用する
しっかり書きたいならassociation :user, factory: :admin_userという感じで書く
実際にSystem Specを書こう
systemディレクトリを新規作成して、日本語で書いてみる
「一覧画面に遷移したら、作成済みのタスクが表示されている」ことについてのSpec
code:3_chapter/taskleaf/spec/system/tasks_spec.rb
require 'rails_helper'
describe 'タスク管理機能', type: :system do
describe '一覧表示機能' do
before do
# ユーザーAを作成しておく
# 作成者がユーザーAのタスクを用意しておく
end
context 'ユーザーAがログインしているとき' do
before do
# ユーザーAでログインする(ログインの処理を書いておく)
end
it 'ユーザーAが作成したタスクが表示される' do
# 作成済みタスクの名称が画面上に表示されていることを確認する
end
end
end
end
それぞれの処理を考えていく
ユーザーAを作成しておく
:userファクトリで作成する。
code:rb
user_a = FactoryBot.create(:user)
上記で記載した場合、データ概要についてはファクトリ側で定義した通りの情報が入る
code:3_chapter/taskleaf/spec/factories/users.rb
FactoryBot.define do
factory :user do
name { 'テストユーザー' }
email { 'test1@example.com' }
password { 'password' }
end
end
情報の指定もできる
code:rb
user_a = FactoryBot.create(:user, name:'ユーザーA', email: 'a@example.com')
今回はこちらで。
作成者がユーザーAのタスクを用意しておく
:taskファクトリで同じように定義する
code:rb
# 変数に格納しておく必要がないため、命令のみ書けば良い
FactoryBot.create(:task, name: '最初のタスク', user: user_a)
ユーザーAでログインする
細かいステップを書き出してみる
ログイン画面へのアクセス
メールアドレスの入力
パスワードの入力
ログインボタンを押す
Capybaraにはブラウザ上での操作を再現してくれるメソッドがあるので、そちらを使う
code:rb
# login_path: login_requiredで定義しているリダイレクト先(login_url)とほぼ同意味だと思う。
# visit URLで、特定のURLへアクセスする挙動を起こす
visit login_path
# メールアドレスとPWの入力
# fill_in テキストフィールドのlabel要素, with: 入れるデータ
fill_in 'メールアドレス', with: 'a@example.com'
fill_in 'パスワード', with: 'password'
# ボタン押下
click_button 'ログインする'
作成済みタスクの名称が画面上に表示されていることを確認する
beforeで作成した、ユーザーAが作ったタスクが画面に反映されているかを確認する
FactoryBot.create(:task, name: '最初のタスク', user: user_a)
code:rb
# expect(page).to ページに期待する内容
# have_comment 'xxx' xxxというコンテンツが画面に存在するか。
expect(page).to have_content '最初のタスク'
マッチャMatcher
RSpecで言う、have_contentの部分のこと
実行してみる
bundle exec rspec spec/system/tasks_spec.rb
エラーが出た。
https://qiita.com/Im_SheeevA/items/a54ba38335a627379c0a
この速習ガイド共通のエラーらしい。
code:txt
Got 0 failures and 2 other errors:
1.1) Failure/Error: visit login_path
Selenium::WebDriver::Error::SessionNotCreatedError:session not created: This version of ChromeDriver only supports Chrome version 114
Current browser version is 117.0.5938.92 with binary path /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
解決する
chromedriver-helper
https://github.com/flavorjones/chromedriver-helper
19年にサポートが終了しているので、現行のchromeではサポートしていない
https://qiita.com/jnchito/items/f9c3be449fd164176efa
webdriversに移行することが推奨
code:txt
Webdrivers::VersionError:
Unable to find latest point release version for 117.0.5938. You appear to be using a non-production version of Chrome. Please set Webdrivers::Chromedriver.required_version = <desired driver version> to a known chromedriver version: https://chromedriver.storage.googleapis.com/index.html
エラーが変わった。
https://qiita.com/jnchito/items/f994dd3ac2cdc39bff8c
ローカルマシンにインストールされているChromeが115以上になるとこの問題が発生する
自分のは 116.0.5845.187なので...
webdriverをgemファイルから消して
bundle update selenium-webdriver capybara
エラーが変わった
code:txt
Selenium::WebDriver::Error::WebDriverError:
Unable to find chromedriver. Please download the server from
https://chromedriver.storage.googleapis.com/index.html and place it somewhere on your PATH.
https://qiita.com/halspring/items/ed81ec980f39f0489a9e
homebrewで落とせるらしい。
既にあると言われたので、uninstallしてもう一度やる
code:txt
brew install chromedriver
Warning: Not upgrading chromedriver, the latest version is already installed
current: ~/Desktop/study/genba_ruby_on_rails/3_chapter/taskleaf
brew uninstall chromedriver
brew install chromedriver
==> Downloading https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/117.0.5938.92/mac-x64/chromedriver-mac-x64.zip
Already downloaded: /Users/skoni/Library/Caches/Homebrew/downloads/747fccd14a664d1d5f973f508e67fad1efd641c2a3059831cd47deb81c28aaa3--chromedriver-mac-x64.zip
==> Installing Cask chromedriver
==> Linking Binary 'chromedriver' to '/usr/local/bin/chromedriver'
🍺 chromedriver was successfully installed!
入った。
code:txt
brew list | grep chrome
chromedriver
macに怒られた
code:txt
“chromedriver”は、開発元を検証できないため開けません。
https://scrapbox.io/files/6512cb2c800b0b001cfb8505.png
システム環境設定から解除
code:txt
bundle exec rspec spec/system/tasks_spec.rb
Capybara starting Puma...
* Version 3.12.6 , codename: Llamas in Pajamas
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:65225
.
Finished in 6.31 seconds (files took 2.07 seconds to load)
1 example, 0 failures
いった〜
ブログに纏めとこう
流れ忘れたのでおさらい
開発を進めやすくするために、テストを書く
テスト用ライブラリを用意する
RSpec
テスティングフレームワーク
テストケースの記述を行うことができる
競合はMinitestなど。
Capybara
E2Eテスト用フレームワーク
Rails同梱で、RSpec / Minitestと組み合わせて使う
webアプリケーションのブラウザ操作に関するテストを行うことができる
FactoryBot
テスト用データの作成をサポートするgem
今回はシステム全体のテストSystem Specを作成する。
tasks_spec.rbを書くためにやったこと
Gemfileにrspecとfactorybotを追記。chromedriver-helperを削除。
capybaraは同梱なので、最初からgemfileへ記述されている。
rspecのコマンドで土台を作る
日本語でspecを書く
factorybotにテストデータの定義をしておく
日本語で書いたspecをコードに書き起こしていく
テストしたい内容に対して、before項でfactorybotを使いユーザーを作る
画面テストを行いたいなら、capybaraを使ったメソッドを使ってコードを書く
テストの実行。
bundle exec rspec spec/system/tasks_spec.rb
他のユーザーが作ったタスクが画面に出ていないかを確認するテストを作る
日本語に起こす。
ユーザーAと、ユーザーAの作ったタスクを用意
rspecは上から順に走るので、既にここの実装は終わっているので作らなくてOK
ユーザーBを作成する
ユーザーBでログインする
ユーザーAのタスクが表示されていないことを確認
beforeを利用した共通化
Specに2通りのcontextが存在している。
ユーザーAがログインしている時
ユーザーBがログインしている時
どちらの処理もlogin_pathへ行ってログインする作業があるので、それを共通化しよう
一旦こうした
code:tasks_spec.rb
before do
user_a = FactoryBot.create(:user, name:'ユーザーA', email: 'a@example.com')
FactoryBot.create(:task, name: '最初のタスク', user: user_a)
# ログイン
visit login_path
fill_in 'メールアドレス', with: 'a@example.com'
fill_in 'パスワード', with: 'password'
click_button 'ログインする'
end
context 'ユーザーAがログインしているとき' do
it 'ユーザーAが作成したタスクが表示される' do
expect(page).to have_content '最初のタスク'
end
end
context 'ユーザBがログインしているとき' do
before do
# ユーザーBを作成。
user_b = FactoryBot.create(:user, name:'ユーザーB', email: 'b@example.com')
end
it 'ユーザーAが作成したタスクは表示されない' do
expect(page).to have_no_content '最初のタスク'
end
end
最初のbeforeにログイン処理を書いた。
実行すると失敗する。
これはユーザーB側のテストでも、ユーザーAでログインしてしまっているからタスクが見えている。
expected not to find text: テキストがないことを期待していた(が、あった。)
code:txt
bundle exec rspec spec/system/tasks_spec.rb
.F
Failures:
1) タスク管理機能 一覧表示機能 ユーザBがログインしているとき ユーザーAが作成したタスクは表示されない
Failure/Error: expect(page).to have_no_content '最初のタスク'
expected not to find text "最初のタスク" in "Taskleaf\nタスク一覧\nログアウト\ntestaa\n現在のログインユーザー: ユーザーA\nログインしました。\nタスク一覧\n新規登録\nID 名称 登録日時\n29 最初のタスク 2023-09-28 12:55:09 UTC 編集削除"
beforeの部分を、ユーザーxでログインするというように変えるような振る舞いにする
letを利用した共通化
before処理のテストケーススコープの変数に値を代入できる
let ( 定義名 ) { 定義の内容 }
もう少し詳しく
let (:user_a) { FactoryBot.create(:user, name:'ユーザーA', email: 'a@example.com') } と書いておく
context 'ユーザーAがログインしている時'の部分で
let(:login_user) { user_a }としておくと、
fill_in 'メールアドレス', with: 'login_user.emailという前提が書かれていた場合は、Aの情報が入る
実際に書いてみる
基本上から実行される流れ。
code:tasks_spec.rb
require 'rails_helper'
describe 'タスク管理機能', type: :system do
describe '一覧表示機能' do
# ユーザーAとユーザーBの情報を定義
let(:user_a) { FactoryBot.create(:user, name:'ユーザーA', email: 'a@example.com') }
let(:user_b) { FactoryBot.create(:user, name:'ユーザーB', email: 'b@example.com') }
before do
# 上で作ったuser_aを使い、ユーザーAのタスクを作る
FactoryBot.create(:task, name: '最初のタスク', user: user_a)
visit login_path
# 変数(のようなもの)として値に入れられる想定の、"login_user"のemailとパスワードを参照してログインを行う
# login_userについては下部contextで定義する。
fill_in 'メールアドレス', with: login_user.email
fill_in 'パスワード', with: login_user.password
click_button 'ログインする'
end
context 'ユーザーAがログインしているとき' do
# beforeで使う、login_userの中身をuser_aとして定義する
let(:login_user) { user_a }
it 'ユーザーAが作成したタスクが表示される' do
expect(page).to have_content '最初のタスク'
end
end
context 'ユーザBがログインしているとき' do
# beforeで使う、login_userの中身をuser_aとして定義する
let(:login_user) { user_b }
it 'ユーザーAが作成したタスクは表示されない' do
expect(page).to have_no_content '最初のタスク'
end
end
end
end
詳細表示機能のSpecの追加
code:rb
# タスクAを作成しておく
let!(:task_a) { FactoryBot.create(:task, name:'最初のタスク', user: user_a)}
describe '詳細表示機能' do
context 'ユーザーAがログインしているとき' do
let(:login_user) { user_a }
# http://localhost:3000/tasks/{task_id}に遷移する
before do
visit task_path(task_a)
Rails.logger.debug "ルーティング: #{task_path(task_a)}"
end
# 最初のタスクという文字が表示されている
it 'ユーザーAが作成したタスクは表示される' do
expect(page).to have_content '最初のタスク'
end
end
end
chapter5-13以降については、また理解したくなった時に読めばいいと思う。
忘れそうなので